<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.

Use of this source code is governed by an Apache 2.0 License.
See the COPYING file for details.
-->
<!--
Author: pupius@google.com (Daniel Pupius)
Author: jananir@google.com (Janani Ravi) (Modified for Closure)
-->
<head>
<title>Closure Unit Tests - goog.module.ModuleManager</title>
<script src="../base.js"></script>
<script src="../../../third_party/closure/goog/deps.js"></script>
<script>
  goog.require('goog.functions');
  goog.require('goog.module.BaseModule');
  goog.require('goog.module.ModuleManager');
  goog.require('goog.testing.MockClock');
  goog.require('goog.testing.jsunit');
  goog.require('goog.testing.recordFunction');
</script>
</head>
<body>
<script>
  var clock;

  function setUpPage() {
    clock = new goog.testing.MockClock(true);
  }

  function tearDownPage() {
    clock.dispose();
  }

  function getModuleManager(infoMap) {
    var mm = new goog.module.ModuleManager();
    mm.setAllModuleInfo(infoMap);

    mm.isModuleLoaded = function(id) {
      return this.getModuleInfo(id).isLoaded();
    }
    return mm;
  }

  function createSuccessfulBatchLoader(moduleMgr) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        setTimeout(goog.bind(this.onLoad, this, ids.concat(), 0), 5);
      },
      onLoad: function(ids, idxLoaded) {
        moduleMgr.beforeLoadModuleCode(ids[0]);
        moduleMgr.setLoaded(ids[idxLoaded]);
        moduleMgr.afterLoadModuleCode(ids[idxLoaded]);
        var idx = idxLoaded + 1;
        if (idx < ids.length) {
          moduleMgr.beforeLoadModuleCode(ids[idx]);
          setTimeout(goog.bind(this.onLoad, this, ids, idx), 2);
        }
      }};
  }

  function createSuccessfulNonBatchLoader(moduleMgr) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        setTimeout(function() {
          moduleMgr.beforeLoadModuleCode(ids[0]);
          moduleMgr.setLoaded(ids[0]);
          moduleMgr.afterLoadModuleCode(ids[0]);
          if (opt_successFn) {
            opt_successFn();
          }
        }, 5);
      }};
  }

  function createUnsuccessfulLoader(moduleMgr, status) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        moduleMgr.beforeLoadModuleCode(ids[0]);
        setTimeout(function() { opt_errFn(status); }, 5);
      }};
  }

  function createTimeoutLoader(moduleMgr, status) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        setTimeout(function() { opt_timeoutFn(status); }, 5);
      }};
  }

  /**
   * Tests loading a module under different conditions i.e. unloaded
   * module, already loaded module, module loaded through user initiated
   * actions, synchronous callback for a module that has been already
   * loaded. Test both batch and non-batch loaders.
   */
  function testExecOnLoad() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));
    execOnLoad_(mm);

    mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulBatchLoader(mm));
    mm.setBatchModeEnabled(true);
    execOnLoad_(mm);
  }

  /**
   * Tests execOnLoad with the specified module manager.
   * @param {goog.module.ModuleManager} mm The module manager.
   */
  function execOnLoad_(mm) {
    // When module is unloaded, execOnLoad is async.
    var execCalled1 = false;
    mm.execOnLoad('a', function() { execCalled1 = true; });
    assertFalse('module "a" should not be loaded', mm.isModuleLoaded('a'));
    assertTrue('module "a" should be loading', mm.isModuleLoading('a'));
    assertFalse('execCalled1 should not be set yet', execCalled1);
    assertTrue('ModuleManager should be active', mm.isActive());
    assertFalse(
        'ModuleManager should not be user active', mm.isUserActive());
    clock.tick(5);
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertFalse(
        'module "a" should not be loading', mm.isModuleLoading('a'));
    assertTrue('execCalled1 should be set', execCalled1);
    assertFalse('ModuleManager should not be active', mm.isActive());
    assertFalse(
        'ModuleManager should not be user active', mm.isUserActive());

    // When module is already loaded, execOnLoad is still async unless
    // specified otherwise.
    var execCalled2 = false;
    mm.execOnLoad('a', function() { execCalled2 = true; });
    assertTrue('module "a" should be loaded', mm.isModuleLoaded('a'));
    assertFalse(
        'module "a" should not be loading', mm.isModuleLoading('a'));
    assertFalse('execCalled2 should not be set yet', execCalled2);
    clock.tick(5);
    assertTrue('execCalled2 should be set', execCalled2);

    // When module is unloaded, execOnLoad is async (user active).
    var execCalled5 = false;
    mm.execOnLoad('c',
        function() { execCalled5 = true; }, null, null, true);
    assertFalse('module "c" should not be loaded', mm.isModuleLoaded('c'));
    assertTrue('module "c" should be loading', mm.isModuleLoading('c'));
    assertFalse('execCalled1 should not be set yet', execCalled5);
    assertTrue('ModuleManager should be active', mm.isActive());
    assertTrue('ModuleManager should be user active', mm.isUserActive());
    clock.tick(5);
    assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));
    assertFalse(
        'module "c" should not be loading', mm.isModuleLoading('c'));
    assertTrue('execCalled1 should be set', execCalled5);
    assertFalse('ModuleManager should not be active', mm.isActive());
    assertFalse(
        'ModuleManager should not be user active', mm.isUserActive());

    // When module is already loaded, execOnLoad is still synchronous when
    // so specified
    var execCalled6 = false;
    mm.execOnLoad('c', function() { execCalled6 = true; },
        undefined, undefined, undefined, true);
    assertTrue('module "c" should be loaded', mm.isModuleLoaded('c'));
    assertFalse(
        'module "c" should not be loading', mm.isModuleLoading('c'));
    assertTrue('execCalled6 should be set', execCalled6);
    clock.tick(5);
    assertTrue('execCalled6 should still be set', execCalled6);

  }

  /**
   * Test aborting the callback called on module load.
   */
  function testExecOnLoadAbort() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    // When module is unloaded and abort is called, module still gets
    // loaded, but callback is cancelled.
    var execCalled1 = false;
    var callback1 = mm.execOnLoad('b', function() { execCalled1 = true; });
    callback1.abort();
    clock.tick(5);
    assertTrue('module "b" should be loaded', mm.isModuleLoaded('b'));
    assertFalse('execCalled3 should not be set', execCalled1);

    // When module is already loaded, execOnLoad is still async, so calling
    // abort should still cancel the callback.
    var execCalled2 = false;
    var callback2 = mm.execOnLoad('a', function() { execCalled2 = true; });
    callback2.abort();
    clock.tick(5);
    assertFalse('execCalled2 should not be set', execCalled2);
  }

  /**
   * Test preloading modules and ensure that the before load, after load
   * and set load called are called only once per module.
   */
  function testExecOnLoadWhilePreloadingAndViceVersa() {
    var mm = getModuleManager({'c': [], 'd': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));
    execOnLoadWhilePreloadingAndViceVersa_(mm);

    mm = getModuleManager({'c': [], 'd': []});
    mm.setLoader(createSuccessfulBatchLoader(mm));
    mm.setBatchModeEnabled(true);
    execOnLoadWhilePreloadingAndViceVersa_(mm);
  }

  /**
   * Perform tests with the specified module manager.
   * @param {goog.module.ModuleManager} mm The module manager.
   */
  function execOnLoadWhilePreloadingAndViceVersa_(mm) {
    var mm = getModuleManager({'c': [], 'd': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var origSetLoaded = mm.setLoaded;
    var calls = [0, 0, 0];
    mm.beforeLoadModuleCode = function(id) {
      calls[0]++;
    };
    mm.setLoaded = function(id) {
      calls[1]++;
      origSetLoaded.call(mm, id);
    };
    mm.afterLoadModuleCode = function(id) {
      calls[2]++;
    };

    mm.preloadModule('c', 2);
    assertFalse(
        'module "c" should not be loading yet', mm.isModuleLoading('c'));
    clock.tick(2);
    assertTrue(
        'module "c" should now be loading', mm.isModuleLoading('c'));
    mm.execOnLoad('c', function() {});
    assertTrue(
        'module "c" should still be loading', mm.isModuleLoading('c'));
    clock.tick(5);
    assertFalse(
        'module "c" should be done loading', mm.isModuleLoading('c'));
    assertEquals(
        'beforeLoad should only be called once for "c"', 1, calls[0]);
    assertEquals(
        'setLoaded should only be called once for "c"', 1, calls[1]);
    assertEquals(
        'afterLoad should only be called once for "c"', 1, calls[2]);

    mm.execOnLoad('d', function() {});
    assertTrue(
        'module "d" should now be loading', mm.isModuleLoading('d'));
    mm.preloadModule('d', 2);
    clock.tick(5);
    assertFalse(
        'module "d" should be done loading', mm.isModuleLoading('d'));
    assertTrue(
        'module "d" should now be loaded', mm.isModuleLoaded('d'));
    assertEquals(
        'beforeLoad should only be called once for "d"', 2, calls[0]);
    assertEquals(
        'setLoaded should only be called once for "d"', 2, calls[1]);
    assertEquals(
        'afterLoad should only be called once for "d"', 2, calls[2]);
  }

  /**
   * Tests loading a module by requesting a Deferred object.
   */
  function testLoad() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;
    var error = null;

    var d = mm.load('a');
    d.addCallback(function(ctx) {
      calledBack = true;
    });
    d.addErrback(function(err) {
      error = err;
    });

    assertFalse(calledBack);
    assertNull(error);
    assertFalse(mm.isUserActive());

    clock.tick(5);

    assertTrue(calledBack);
    assertNull(error);
  }

  /**
   * Tests loading a module by user action by requesting a Deferred object.
   */
  function testLoadForUser() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;
    var error = null;

    var d = mm.load('a', true);
    d.addCallback(function(ctx) {
      calledBack = true;
    });
    d.addErrback(function(err) {
      error = err;
    });

    assertFalse(calledBack);
    assertNull(error);
    assertTrue(mm.isUserActive());

    clock.tick(5);

    assertTrue(calledBack);
    assertNull(error);
  }

  /**
   * Tests that preloading a module calls back the deferred object.
   */
  function testPreloadDeferredWhenNotLoaded() {
    var mm = getModuleManager({'a': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;

    var d = mm.preloadModule('a');;
    d.addCallback(function(ctx) {
      calledBack = true;
    });

    // First load should take five ticks.
    assertFalse('module "a" should not be loaded yet', calledBack);
    clock.tick(5);
    assertTrue('module "a" should be loaded', calledBack);
  }

  /**
   * Tests preloading an already loaded module.
   */
  function testPreloadDeferredWhenLoaded() {
    var mm = getModuleManager({'a': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;

    mm.preloadModule('a');
    clock.tick(5);

    var d = mm.preloadModule('a');;
    d.addCallback(function(ctx) {
      calledBack = true;
    });

    // Module is already loaded, should be called back after the setTimeout
    // in preloadModule.
    assertFalse('deferred for module "a" should not be called yet', calledBack);
    clock.tick(1);
    assertTrue('module "a" should be loaded', calledBack);
  }


  /**
   * Tests preloading a module that is currently loading.
   */
  function testPreloadDeferredWhenLoading() {
    var mm = getModuleManager({'a': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    mm.preloadModule('a');
    clock.tick(1);

    // 'b' is in the middle of loading, should get called back when it's done.
    var calledBack = false;
    var d = mm.preloadModule('a');;
    d.addCallback(function(ctx) {
      calledBack = true;
    });

    assertFalse('module "a" should not be loaded yet', calledBack);
    clock.tick(4);
    assertTrue('module "a" should be loaded', calledBack);
  }

  /**
   * Tests that load doesn't trigger another load if a module is already
   * preloading.
   */
  function testLoadWhenPreloading() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var origSetLoaded = mm.setLoaded;
    var calls = [0, 0, 0];
    mm.beforeLoadModuleCode = function(id) {
      calls[0]++;
    };
    mm.setLoaded = function(id) {
      calls[1]++;
      origSetLoaded.call(mm, id);
    };
    mm.afterLoadModuleCode = function(id) {
      calls[2]++;
    };

    var calledBack = false;
    var error = null;

    mm.preloadModule('c', 2);
    assertFalse(
        'module "c" should not be loading yet', mm.isModuleLoading('c'));
    clock.tick(2);
    assertTrue(
        'module "c" should now be loading', mm.isModuleLoading('c'));

    var d = mm.load('c');
    d.addCallback(function(ctx) {
      calledBack = true;
    });
    d.addErrback(function(err) {
      error = err;
    });

    assertTrue(
        'module "c" should still be loading', mm.isModuleLoading('c'));
    clock.tick(5);
    assertFalse(
        'module "c" should be done loading', mm.isModuleLoading('c'));
    assertEquals(
        'beforeLoad should only be called once for "c"', 1, calls[0]);
    assertEquals(
        'setLoaded should only be called once for "c"', 1, calls[1]);
    assertEquals(
        'afterLoad should only be called once for "c"', 1, calls[2]);

    assertTrue(calledBack);
    assertNull(error);
  }

 /**
   * Tests loading a module via load when the module is already
   * loaded.  The deferred's callback should be called immediately.
   */
 function testLoadWhenLoaded() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var calledBack = false;
    var error = null;

    mm.preloadModule('b', 2);
    clock.tick(10);

    assertFalse(
        'module "b" should be done loading', mm.isModuleLoading('b'));

    var d = mm.load('b');
    d.addCallback(function(ctx) {
      calledBack = true;
    });
    d.addErrback(function(err) {
      error = err;
    });

    assertTrue(calledBack);
    assertNull(error);
  }

  /**
   * Tests that the deferred's errbacks are called if the module fails to load.
   */
  function testLoadWithFailingModule() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    mm.setLoader(createUnsuccessfulLoader(mm, 401));
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
      assertEquals('Failure cause was not as expected',
                   goog.module.ModuleManager.FailureType.UNAUTHORIZED,
                   cause);
      firedLoadFailed = true;
    });
    var calledBack = false;
    var error = null;

    var d = mm.load('a');
    d.addCallback(function(ctx) {
      calledBack = true;
    });
    d.addErrback(function(err) {
      error = err;
    });

    assertFalse(calledBack);
    assertNull(error);

    clock.tick(500);

    assertFalse(calledBack);

    // NOTE: Deferred always calls errbacks with an Error object.  For now the
    // module manager just passes the FailureType which gets set as the Error
    // object's message.
    assertEquals('Failure cause was not as expected',
        goog.module.ModuleManager.FailureType.UNAUTHORIZED,
        Number(error.message));
  }

  /**
   * Test loading dependencies transitively.
   */
  function testLoadingDepsInNonBatchMode1() {
    var mm = getModuleManager({
      'i': [],
      'j': [],
      'k': ['j'],
      'l': ['i','j','k']});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    mm.preloadModule('j');
    clock.tick(5);
    assertTrue('module "j" should be loaded', mm.isModuleLoaded('j'));
    assertFalse(
        'module "i" should not be loaded (1)', mm.isModuleLoaded('i'));
    assertFalse(
        'module "k" should not be loaded (1)', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (1)', mm.isModuleLoaded('l'));

    // When loading a module in non-batch mode, its dependencies should be
    // requested independently, and in dependency order.
    mm.preloadModule('l');
    clock.tick(5);
    assertTrue('module "i" should be loaded', mm.isModuleLoaded('i'));
    assertFalse(
        'module "k" should not be loaded (2)', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (2)', mm.isModuleLoaded('l'));
    clock.tick(5);
    assertTrue('module "k" should be loaded', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (3)', mm.isModuleLoaded('l'));
    clock.tick(5);
    assertTrue(
        'module "l" should be loaded', mm.isModuleLoaded('l'));
  }

  /**
   * Test loading dependencies transitively and in dependency order.
   */
  function testLoadingDepsInNonBatchMode2() {
    var mm = getModuleManager({
      'h': [],
      'i': ['h'],
      'j': ['i'],
      'k': ['j'],
      'l': ['i','j','k'],
      'm': ['l']});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    // When loading a module in non-batch mode, its dependencies should be
    // requested independently, and in dependency order. The order in this
    // case should be h,i,j,k,l,m.
    mm.preloadModule('m');
    clock.tick(5);
    assertTrue('module "h" should be loaded', mm.isModuleLoaded('h'));
    assertFalse(
        'module "i" should not be loaded (1)', mm.isModuleLoaded('i'));
    assertFalse(
        'module "j" should not be loaded (1)', mm.isModuleLoaded('j'));
    assertFalse(
        'module "k" should not be loaded (1)', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (1)', mm.isModuleLoaded('l'));
    assertFalse(
        'module "m" should not be loaded (1)', mm.isModuleLoaded('m'));

    clock.tick(5);
    assertTrue('module "i" should be loaded', mm.isModuleLoaded('i'));
    assertFalse(
        'module "j" should not be loaded (2)', mm.isModuleLoaded('j'));
    assertFalse(
        'module "k" should not be loaded (2)', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (2)', mm.isModuleLoaded('l'));
    assertFalse(
        'module "m" should not be loaded (2)', mm.isModuleLoaded('m'));

    clock.tick(5);
    assertTrue('module "j" should be loaded', mm.isModuleLoaded('j'));
    assertFalse(
        'module "k" should not be loaded (3)', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (3)', mm.isModuleLoaded('l'));
    assertFalse(
        'module "m" should not be loaded (3)', mm.isModuleLoaded('m'));

    clock.tick(5);
    assertTrue('module "k" should be loaded', mm.isModuleLoaded('k'));
    assertFalse(
        'module "l" should not be loaded (4)', mm.isModuleLoaded('l'));
    assertFalse(
        'module "m" should not be loaded (4)', mm.isModuleLoaded('m'));

    clock.tick(5);
    assertTrue('module "l" should be loaded', mm.isModuleLoaded('l'));
    assertFalse(
        'module "m" should not be loaded (5)', mm.isModuleLoaded('m'));

    clock.tick(5);
    assertTrue('module "m" should be loaded', mm.isModuleLoaded('m'));
  }

  function testLoadingDepsInBatchMode() {
    var mm = getModuleManager({
      'e': [],
      'f': [],
      'g': ['f'],
      'h': ['e','f','g']});
    mm.setLoader(createSuccessfulBatchLoader(mm));
    mm.setBatchModeEnabled(true);

    mm.preloadModule('f');
    clock.tick(5);
    assertTrue('module "f" should be loaded', mm.isModuleLoaded('f'));
    assertFalse(
        'module "e" should not be loaded (1)', mm.isModuleLoaded('e'));
    assertFalse(
        'module "g" should not be loaded (1)', mm.isModuleLoaded('g'));
    assertFalse(
        'module "h" should not be loaded (1)', mm.isModuleLoaded('h'));

    // When loading a module in batch mode, its not-yet-loaded dependencies
    // should be requested at the same time, and in dependency order.
    mm.preloadModule('h');
    clock.tick(5);
    assertTrue('module "e" should be loaded', mm.isModuleLoaded('e'));
    assertFalse(
        'module "g" should not be loaded (2)', mm.isModuleLoaded('g'));
    assertFalse(
        'module "h" should not be loaded (2)', mm.isModuleLoaded('h'));
    clock.tick(2);
    assertTrue(
        'module "g" should be loaded', mm.isModuleLoaded('g'));
    assertFalse(
        'module "h" should not be loaded (3)', mm.isModuleLoaded('h'));
    clock.tick(2);
    assertTrue(
        'module "h" should be loaded', mm.isModuleLoaded('h'));
  }

  /**
   * Test unauthorized errors while loading modules.
   */
  function testUnauthorizedLoading() {
    var mm = getModuleManager({
      'm': [],
      'n': [],
      'o': ['n']});
    mm.setLoader(createUnsuccessfulLoader(mm, 401));

    // Callback checks for an unauthorized error
    var firedLoadFailed = false;
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
          assertEquals('Failure cause was not as expected',
                       goog.module.ModuleManager.FailureType.UNAUTHORIZED,
                       cause);
          firedLoadFailed = true;
        });
    mm.execOnLoad('o', function() {});
    assertTrue('module "o" should be loading', mm.isModuleLoading('o'));
    assertTrue('module "n" should be loading', mm.isModuleLoading('n'));
    clock.tick(5);
    assertTrue(
        'should have called unauthorized module callback', firedLoadFailed);
    assertFalse(
        'module "o" should not be loaded', mm.isModuleLoaded('o'));
    assertFalse(
        'module "o" should not be loading', mm.isModuleLoading('o'));
    assertFalse(
        'module "n" should not be loaded', mm.isModuleLoaded('n'));
    assertFalse(
        'module "n" should not be loading', mm.isModuleLoading('n'));
  }

  /**
   * Test error loading modules which are retried.
   */
  function testErrorLoadingModule() {
    var mm = getModuleManager({
      'p': ['q'],
      'q': [],
      'r': ['q','p']});
    mm.setLoader(createUnsuccessfulLoader(mm, 500));

    mm.preloadModule('r');
    clock.tick(4);

    // A module request is now underway using the unsuccessful loader.
    // We substitute a successful loader for future module load requests.
    mm.setLoader(createSuccessfulNonBatchLoader(mm));
    clock.tick(1);
    assertFalse(
        'module "q" should not be loaded (1)', mm.isModuleLoaded('q'));
    assertFalse(
        'module "p" should not be loaded (1)', mm.isModuleLoaded('p'));
    assertFalse(
        'module "r" should not be loaded (1)', mm.isModuleLoaded('r'));

    // Failed loads are automatically retried.
    clock.tick(5);
    assertTrue('module "q" should be loaded', mm.isModuleLoaded('q'));
    assertFalse(
        'module "p" should not be loaded (2)', mm.isModuleLoaded('p'));
    assertFalse(
        'module "r" should not be loaded (2)', mm.isModuleLoaded('r'));
    clock.tick(5);
    assertTrue('module "p" should be loaded', mm.isModuleLoaded('p'));
    assertFalse(
        'module "r" should not be loaded (3)', mm.isModuleLoaded('r'));
    clock.tick(5);
    assertTrue(
        'module "r" should be loaded', mm.isModuleLoaded('r'));
  }

  /**
   * Test consecutive errors in loading modules.
   */
  function testConsecutiveErrors() {
    var mm = getModuleManager({'s': []});
    mm.setLoader(createUnsuccessfulLoader(mm, 500));

    // Register an error callback for consecutive failures.
    var firedLoadFailed = false;
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
          assertEquals('Failure cause was not as expected',
              goog.module.ModuleManager.FailureType.CONSECUTIVE_FAILURES,
              cause);
          firedLoadFailed = true;
        });

    mm.preloadModule('s');
    assertFalse(
        'module "s" should not be loaded (0)', mm.isModuleLoaded('s'));

    // Fail twice.
    for (var i = 0; i < 2; i++) {
      clock.tick(5);
      assertFalse(
          'module "s" should not be loaded (1)', mm.isModuleLoaded('s'));
      assertFalse(
          'should not fire failed callback (1)', firedLoadFailed);
    }

    // Fail a third time and check that the callback is fired.
    clock.tick(5);
    assertFalse(
        'module "s" should not be loaded (2)', mm.isModuleLoaded('s'));
    assertTrue(
        'should have fired failed callback', firedLoadFailed);

    // Check that it doesn't attempt to load the module anymore after it has
    // failed.
    var triedLoad = false;
    mm.setLoader({
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn) {
        triedLoad = true;
      }});

    // Also reset the failed callback flag and make sure it isn't called
    // again.
    firedLoadFailed = false;
    clock.tick(10);
    assertFalse(
        'module "s" should not be loaded (3)', mm.isModuleLoaded('s'));
    assertFalse('No more loads should have been tried', triedLoad);
    assertFalse('The load failed callback should be fired only once',
        firedLoadFailed);
  }

  /**
   * Test loading errors due to old code.
   */
  function testOldCodeGoneError() {
    var mm = getModuleManager({'s': []});
    mm.setLoader(createUnsuccessfulLoader(mm, 410));

    // Callback checks for an old code failure
    var firedLoadFailed = false;
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
          assertEquals('Failure cause was not as expected',
              goog.module.ModuleManager.FailureType.OLD_CODE_GONE,
              cause);
          firedLoadFailed = true;
        });

    mm.preloadModule('s', 0);
    assertFalse(
        'module "s" should not be loaded (0)', mm.isModuleLoaded('s'));
    clock.tick(5);
    assertFalse(
        'module "s" should not be loaded (1)', mm.isModuleLoaded('s'));
    assertTrue(
        'should have called old code gone callback', firedLoadFailed);
  }

  /**
   * Test timeout.
   */
  function testTimeout() {
    var mm = getModuleManager({'s': []});
    mm.setLoader(createTimeoutLoader(mm));

    // Callback checks for timeout
    var firedTimeout = false;
    mm.registerCallback(goog.module.ModuleManager.CallbackType.ERROR,
        function(callbackType, id, cause) {
          assertEquals('Failure cause was not as expected',
              goog.module.ModuleManager.FailureType.TIMEOUT,
              cause);
          firedTimeout = true;
        });

    mm.preloadModule('s', 0);
    assertFalse(
        'module "s" should not be loaded (0)', mm.isModuleLoaded('s'));
    clock.tick(5);
    assertFalse(
        'module "s" should not be loaded (1)', mm.isModuleLoaded('s'));
    assertTrue(
        'should have called timeout callback', firedTimeout);
  }


  /**
   * Make sure ModuleInfo objects in moduleInfoMap_ get disposed.
   */
  function testDispose() {
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});

    var moduleInfoA = mm.getModuleInfo('a');
    assertNotNull(moduleInfoA);
    var moduleInfoB = mm.getModuleInfo('b');
    assertNotNull(moduleInfoB);
    var moduleInfoC = mm.getModuleInfo('c');
    assertNotNull(moduleInfoC);

    mm.dispose();
    assertTrue(moduleInfoA.isDisposed());
    assertTrue(moduleInfoB.isDisposed());
    assertTrue(moduleInfoC.isDisposed());
  }

  function testDependencyOrderingWithSimpleDeps() {
    var mm = getModuleManager({
      'a': ['b', 'c'],
      'b': ['d'],
      'c': ['e', 'f'],
      'd': [],
      'e': [],
      'f': []
    });
    var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
    assertDependencyOrder(ids, mm);
    assertArrayEquals(['d', 'e', 'f', 'b', 'c', 'a'], ids);
  }

  function testDependencyOrderingWithCommonDepsInDeps() {
    // Tests to make sure that if dependencies of the root are loaded before
    // their common dependencies.
    var mm = getModuleManager({
      'a': ['b', 'c'],
      'b': ['d'],
      'c': ['d'],
      'd': []
    });
    var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
    assertDependencyOrder(ids, mm);
    assertArrayEquals(['d', 'b', 'c', 'a'], ids);
  }

  function testDependencyOrderingWithCommonDepsInRoot1() {
    // Tests the case where a dependency of the root depends on another
    // dependency of the root.  Irregardless of ordering in the root's
    // deps.
    var mm = getModuleManager({
      'a': ['b', 'c'],
      'b': ['c'],
      'c': []
    });
    var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
    assertDependencyOrder(ids, mm);
    assertArrayEquals(['c', 'b', 'a'], ids);
  }

  function testDependencyOrderingWithCommonDepsInRoot2() {
    // Tests the case where a dependency of the root depends on another
    // dependency of the root.  Irregardless of ordering in the root's
    // deps.
    var mm = getModuleManager({
      'a': ['b', 'c'],
      'b': [],
      'c': ['b']
    });
    var ids = mm.getNotYetLoadedTransitiveDepIds_('a');
    assertDependencyOrder(ids, mm);
    assertArrayEquals(['b', 'c', 'a'], ids);
  }

  function testDependencyOrderingWithGmailExample() {
    // Real dependency graph taken from gmail.
    var mm = getModuleManager({
      's': ['dp', 'ml', 'md'],
      'dp': ['a'],
      'ml': ['ld', 'm'],
      'ld': ['a'],
      'm': ['ad', 'mh', 'n'],
      'md': ['mh', 'ld'],
      'a': [],
      'mh': [],
      'ad': [],
      'n': []
    });

    mm.setLoaded('a');
    mm.setLoaded('m');
    mm.setLoaded('n');
    mm.setLoaded('ad');
    mm.setLoaded('mh');

    var ids = mm.getNotYetLoadedTransitiveDepIds_('s');
    assertDependencyOrder(ids, mm);
    assertArrayEquals(['ld', 'dp', 'ml', 'md', 's'], ids);
  }

  function assertDependencyOrder(list, mm) {
    var seen = {};
    for (var i = 0; i < list.length; i++) {
      var id = list[i];
      seen[id] = true;
      var deps = mm.getModuleInfo(id).getDependencies();
      for (var j = 0; j < deps.length; j++) {
        var dep = deps[j];
        assertTrue('Unresolved dependency [' + dep + '] for [' + id + '].',
            seen[dep] || mm.getModuleInfo(dep).isLoaded());
      }
    }
  }

 function testRegisterInitializationCallback() {
   var initCalled = 0;
   var mm = getModuleManager({'a': [], 'b': [], 'c': []});
   mm.setLoader(createSuccessfulNonBatchLoaderWithRegisterInitCallback(mm,
       function() {
        ++initCalled;
       }));
   execOnLoad_(mm);
   // execOnLoad_ loads modules a and c
   assertTrue(initCalled == 2);
 }

  function createSuccessfulNonBatchLoaderWithRegisterInitCallback(
      moduleMgr, fn) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        moduleMgr.beforeLoadModuleCode(ids[0]);
        moduleMgr.registerInitializationCallback(fn);
        setTimeout(function() {
          moduleMgr.setLoaded(ids[0]);
          moduleMgr.afterLoadModuleCode(ids[0]);
          if (opt_successFn) {
            opt_successFn();
          }
        }, 5);
      }};
  }

  function testSetModuleConstructor() {
    var initCalled = 0;
    var mm = getModuleManager({'a': [], 'b': [], 'c': []});
    var info = {
      'a': { ctor: AModule, count: 0 },
      'b': { ctor: BModule, count: 0 },
      'c': { ctor: CModule, count: 0 }
    };
    function AModule() {
      ++info['a'].count;
      goog.module.BaseModule.call(this);
    }
    goog.inherits(AModule, goog.module.BaseModule);
    function BModule() {
      ++info['b'].count;
      goog.module.BaseModule.call(this);
    }
    goog.inherits(BModule, goog.module.BaseModule);
    function CModule() {
      ++info['c'].count;
      goog.module.BaseModule.call(this);
    }
    goog.inherits(CModule, goog.module.BaseModule);

    mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(mm, info));
    execOnLoad_(mm);
    assertTrue(info['a'].count == 1);
    assertTrue(info['b'].count == 0);
    assertTrue(info['c'].count == 1);
    assertTrue(mm.getModuleInfo('a').getModule() instanceof AModule);
    assertTrue(mm.getModuleInfo('c').getModule() instanceof CModule);
  }

  /**
   * Tests that a call to load the loading module during module initialization
   * doesn't trigger a second load.
   */
  function testLoadWhenInitializing() {
    var mm = getModuleManager({'a': []});
    mm.setLoader(createSuccessfulNonBatchLoader(mm));

    var info = {
      'a': { ctor: AModule, count: 0 }
    };
    function AModule() {
      ++info['a'].count;
      goog.module.BaseModule.call(this);
    }
    goog.inherits(AModule, goog.module.BaseModule);
    AModule.prototype.initialize = function() {
      mm.load('a');
    };
    mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(mm, info));
    mm.preloadModule('a');
    clock.tick(5);
    assertEquals(info['a'].count, 1);
  }

  function testErrorInEarlyCallback() {
    var errback = goog.testing.recordFunction();
    var callback = goog.testing.recordFunction();
    var mm = getModuleManager({'a': [], 'b': ['a']});
    mm.getModuleInfo('a').registerEarlyCallback(goog.functions.error('error'));
    mm.getModuleInfo('a').registerCallback(callback);
    mm.getModuleInfo('a').registerErrback(errback);

    mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(
        mm, createModulesFor('a', 'b')));
    mm.preloadModule('b');
    clock.tick(5);

    assertEquals(1, callback.getCallCount());
    assertEquals(1, errback.getCallCount());
    assertEquals(goog.module.ModuleManager.FailureType.INIT_ERROR,
        errback.getLastCall().getArguments()[0]);
    assertTrue(mm.getModuleInfo('a').isLoaded());
    assertFalse(mm.getModuleInfo('b').isLoaded());

    clock.tick(5);
    assertTrue(mm.getModuleInfo('b').isLoaded());
  }

  function testErrorInNormalCallback() {
    var earlyCallback = goog.testing.recordFunction();
    var errback = goog.testing.recordFunction();
    var mm = getModuleManager({'a': [], 'b': ['a']});
    mm.getModuleInfo('a').registerEarlyCallback(earlyCallback);
    mm.getModuleInfo('a').registerEarlyCallback(goog.functions.error('error'));
    mm.getModuleInfo('a').registerErrback(errback);

    mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(
        mm, createModulesFor('a', 'b')));
    mm.preloadModule('b');
    clock.tick(10);

    assertEquals(1, errback.getCallCount());
    assertEquals(goog.module.ModuleManager.FailureType.INIT_ERROR,
        errback.getLastCall().getArguments()[0]);
    assertTrue(mm.getModuleInfo('a').isLoaded());
    assertTrue(mm.getModuleInfo('b').isLoaded());
  }

  function testErrorInErrback() {
    var mm = getModuleManager({'a': [], 'b': ['a']});
    mm.getModuleInfo('a').registerCallback(goog.functions.error('error1'));
    mm.getModuleInfo('a').registerErrback(goog.functions.error('error2'));

    mm.setLoader(createSuccessfulNonBatchLoaderWithConstructor(
        mm, createModulesFor('a', 'b')));
    mm.preloadModule('a');
    try {
      clock.tick(10);
    } catch (e) {
      assertContains('Module errback failure', e.message);
      if (!goog.userAgent.IE) {
        assertContains('error2', e.message);
      }
    }

    assertTrue(mm.getModuleInfo('a').isLoaded());
  }

  function createModulesFor(var_args) {
    var result = {};
    for (var i = 0; i < arguments.length; i++) {
      var key = arguments[i];
      result[key] = {ctor: goog.module.BaseModule};
    }
    return result;
  }

  function createSuccessfulNonBatchLoaderWithConstructor(moduleMgr, info) {
    return {
      loadModules: function(ids, moduleInfoMap, opt_successFn, opt_errFn,
          opt_timeoutFn) {
        setTimeout(function() {
          moduleMgr.beforeLoadModuleCode(ids[0]);
          moduleMgr.setModuleConstructor(info[ids[0]].ctor);
          moduleMgr.setLoaded(ids[0]);
          moduleMgr.afterLoadModuleCode(ids[0]);
          if (opt_successFn) {
            opt_successFn();
          }
        }, 5);
      }};
  }

  function testInitCallbackInBaseModule() {
    var mm = new goog.module.ModuleManager();
    var called = false;
    var context;
    mm.registerInitializationCallback(function(mcontext) {
      called = true;
      context = mcontext;
    });
    mm.setAllModuleInfo({'a': [], 'b': ['a']});
    assertTrue('Base initialization not called', called);
    assertNull('Context should still be null', context);

    var mm = new goog.module.ModuleManager();
    called = false;
    mm.registerInitializationCallback(function(mcontext) {
      called = true;
      context = mcontext;
    });
    var appContext = {};
    mm.setModuleContext(appContext);
    assertTrue('Base initialization not called after setModuleContext', called);
    assertEquals('Did not receive module context', appContext, context);
  }

</script>
</body>
</html>
